/**
 * 1. Ograniczenia (bardziej skomplikowane) np. do tych funkcji co mamy
 * 2. Rozgraniczenie liczby podzadań i wątków
 * 3. Opisać pułapkę z pardiso
 * 4. Poszukać jakie funkcje bez ograniczeń są podatne na nasz algorytm
 * 5. Jeden poziom dekompozycji, wiele wątków, pokazać że działa zrównoleglenie
 * 6. Uporządkować kod WITH_OMP
 *
 * 7. Druga część - implementacja jednego zadania
 *
 */

#include "Algorithm.hpp"
#include "testHeaders.hpp"
#include <iostream>
#include <cstring>
#include <cmath>
#include <fstream>
#include <popt.h>
#include "parallel.hpp"
#include "utils.h"

using namespace std;

//LIBS = -lipopt -llapack -lblas -lm  -ldl

int dimension = 30;
int subProblemsNum = 2;
int threadsNum = 1;
const char *problemName = NULL; //tu mozna wstawic default np. (char*) "Parabole"
Problem* problem = NULL;
int checkDerivatives = false;
int verbose = 0;
int maxSteps = 500;

struct poptOption
		optionsTable[] = {
				{ "dimension", 'n', POPT_ARG_INT, &dimension, 0,
						"Dimension of problem to solve (def: 30)",
						"Integer value" },
				{
						"procs",
						'p',
						POPT_ARG_INT,
						&subProblemsNum,
						0,
						"Number of parallel subproblems to divide (def: 2; 0 mean solve using pure Ipopt",
						"Integer value" }, { "threads", 't', POPT_ARG_INT,
						&threadsNum, 0, "Number of threads to use",
						"Integer value" },

				{ "verbose", 'v', POPT_ARG_INT | POPT_ARGFLAG_OPTIONAL,
						&verbose, 'v', "Verbose level", "Integer" }, { "tol",
						'l', POPT_ARG_DOUBLE, &Algorithm::toleranceLevel, 0,
						"Tolerance level", "Double value (def: 1e-9)" }, {
						"maxSteps", 'm', POPT_ARG_INT, &maxSteps, 'm',
						"Max steps", "Unsigned integer, default=500" }, {
						"checkDeriv", 'c', POPT_ARG_NONE, &checkDerivatives, 0,
						"Check derivatives instead of solving problem", NULL },
						POPT_AUTOHELP POPT_TABLEEND
	};

Problem* createProblem(int dim) {
	if (strcmp(problemName, "Parabole") == 0)
		return new Parabole(dimension);
	else if (strcmp(problemName, "ParaboleC") == 0)
		return new ParaboleC(dimension);
	else if (strcmp(problemName, "Problem1") == 0)
		return new Problem1(dimension);
	else if (strcmp(problemName, "Problem3") == 0)
		return new Problem3(dimension);
	else if (strcmp(problemName, "Problem5") == 0)
		return new Problem5(dimension);
	else if (strcmp(problemName, "Problem13") == 0)
		return new Problem13(dimension);
	else if (strcmp(problemName, "Problem18") == 0)
		return new Problem18(dimension);
	else {
		return NULL;
	}
}

void readOptions(int argc, char **argv) {
	if (argc == 0)
		return;

	poptContext con = poptGetContext(NULL, argc,
			const_cast<const char**> (argv), optionsTable, 0);

	int rc = 0;
	bool err = false;
	while ((rc = poptGetNextOpt(con)) != -1) {
		if (rc < 0) {
			fprintf(stderr, "%s: %s\n", poptBadOption(con, 0), poptStrerror(rc));
			err = true;
		} else if (rc == 'v') {
			if (verbose == 0) {
				verbose = 1;
			}
		} else if (rc == 'm') {
			Algorithm::maxSteps = maxSteps;
		}
	}

	if (err) {
		exit(0);
	}

	if (verbose < 0) {
		poptPrintUsage(con, stderr, 0);
		cout << endl << "Fatal error: verbose level must be >= 0" << endl;
		exit(0);
	}

	Algorithm::verboseLevel = verbose;

	if (dimension < 1) {
		poptPrintUsage(con, stderr, 0);
		cout << endl << "Fatal error: dimension must be greater or equal 1"
				<< endl;
		exit(0);
	}
	if (subProblemsNum < 0) {
		poptPrintUsage(con, stderr, 0);
		cout << endl
				<< "Fatal error: number of subproblems must be >= 0 (0 - pure Ipopt)"
				<< endl;
		exit(0);
	}
	if (Algorithm::maxSteps < 1) {
		poptPrintUsage(con, stderr, 0);
		cout << endl
				<< "Fatal error: max steps number must be greater or equal 1"
				<< endl;
		exit(0);
	}

	//problem
	if ((problemName = poptGetArg(con)) == NULL) {
		poptPrintUsage(con, stderr, 0);
		cout << endl << "Fatal error: must specify problem to solve" << endl;
		cout
				<< "\tOne of: Parabole, ParaboleC, Problem1, Problem5, Problem13, Problem18"
				<< endl;
		exit(0);
	}

	bool hadLeftovers = false;
	const char *leftOverArg;
	while ((leftOverArg = poptGetArg(con)) != NULL) {
		cout << "Warning: skipping given attribute: " << leftOverArg << endl;
		hadLeftovers = true;
	}

	if (hadLeftovers) {
		cout << "Press any key to continue.." << endl;
		char c;
		cin >> c;
	}

	problem = createProblem(dimension);
	try {
		if (problem == NULL) {
			poptPrintUsage(con, stderr, 0);
			cout << endl << "Fatal error: unrecognized problem name: "
					<< problemName << endl;
			exit(0);
		}
	} catch (const bad_alloc& exp) {
		cout << endl
				<< "Fatal error: unsufficient memory to create problem with dimension "
				<< dimension << endl;
		exit(0);
	}
	poptFreeContext(con);
}

int main(int argc, char* argv[]) {

#ifdef WITH_MPI
	MC(MPI_Init(&argc, &argv));
#endif

	readOptions(argc, argv);

	if (checkDerivatives) {
		if (!verbose)
			Algorithm::verboseLevel = 1;
		Algorithm alg(*problem, 1);
		alg.checkDerivatives();

		return 0;
	}

#ifdef WITH_OMP
	if (threadsNum > omp_get_num_procs()) {
		cerr << "Error: Too many threads specified (" << threadsNum << "). Only "
		<< omp_get_num_procs() << " are available." << endl;
		return 1;
	}

	if (threadsNum > 0) {
		//omp_set_dynamic(true);
		omp_set_num_threads(threadsNum);
		assert(omp_get_max_threads() == threadsNum);
	}
#endif

	cout << "Problem: " << problemName << "(" << dimension << ")" << endl;
	cout << "SubProblems: " << subProblemsNum << ", threads: " << threadsNum
			<< endl;

	if (subProblemsNum == 0) {
		//pure ipopt optimalization
		SmartPtr<IpoptApplication> app = new IpoptApplication();
		app->Options()->SetStringValue("hessian_approximation",
				"limited-memory");
		app->Options()->SetNumericValue("tol", Algorithm::toleranceLevel);
		app->Options()->SetIntegerValue("print_level", (verbose > 0 ? verbose
				- 1 : verbose));
		app->Initialize();

		long tStart = getTimeMilis();
		SmartPtr<TNLP> pt = problem;
		ApplicationReturnStatus st = app->OptimizeTNLP(pt);
		problem = NULL;

		cout << "Time: " << getTimeMilis() - tStart << "ms" << endl;

		cout << "Status: " << st << endl;
	} else {

		if (verbose) {
			cout << "Initializing algorithm engine.." << endl;
		}

		Algorithm alg(*problem, subProblemsNum);

		if (verbose) {
			cout << "Solving.." << endl;
		}

		ApplicationReturnStatus st = alg.solve();

		if (st == Solve_Succeeded) {
			cout << "Solve succeeded!" << endl;
		} else {
			cout << "Max steps exceeded: " << alg.maxSteps << endl;
		}

		if (verbose) {
			cout << "Done in " << alg.getSteps() << " steps\n";
			cout << "Solution: " << endl;
			//for (int i = 0; i < alg.dim; ++i) {
			//	cout << alg.lastOptX[i] << endl;
			//}
			cout << alg.lastOptX[0] << endl;
			cout << alg.lastOptX[alg.dim - 1] << endl;
		}

		cout << "Dimension: " << scientific << (double) dimension << endl;
		cout << "Parallel time: " << alg.timeParallel << "ms, "
				<< alg.timeParallelSummed / subProblemsNum << "ms per proc"
				<< endl;
		cout << "Synchro time: " << alg.timeSynchro << "ms" << endl;
		cout << "Overall time: " << alg.timeOverall << "ms" << endl;
	}

	if (problem != NULL) {
		delete problem;
	}

#ifdef WITH_MPI
	MPI_Finalize();
#endif
	return 0;
}
